package springiesSimulator;

import java.awt.Color;
import java.awt.Dimension;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Point;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyAdapter;
import java.awt.event.KeyEvent;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.List;
import javax.swing.JComponent;
import javax.swing.JFileChooser;
import javax.swing.Timer;
import javax.swing.event.MouseInputAdapter;
import element.Mass;
import element.Spring;
import util.EnvironmentFactory;
import util.Factory;
import util.Vector;


/**
 * Creates an component that is a viewer onto an animation.
 *
 * @author Robert C. Duvall, modified by Mark Hoffman and Mark Govea
 */
@SuppressWarnings("serial")
public class Canvas extends JComponent {
    /**
     * used if no key is currently being input.
     */
    public static final int NO_KEY_PRESSED = -1;

    // animate 25 times per second if possible (in milliseconds)
    /**
     * 1000 milliseconds.
     */
    public static final int ONE_SECOND = 1000;
    /**
     * number of frames each second.
     */
    public static final int FRAMES_PER_SECOND = 25;

    private static final double MOUSE_SPRING_CONSTANT = 0.5;
    private static final JFileChooser OUR_CHOOSER = new JFileChooser(
            System.getProperties().getProperty("user.dir"));

    // user's game to be animated
    private List<Assembly> myAssemblies;
    // drives simulation
    private Timer myTimer;
    // input state
    private int myLastKeyPressed;
    private Point myLastMousePosition;
    // only one so that it maintains user's preferences

    private EnvironmentFactory myEnvironment;
    private EnvironmentManager myEnvironmentManager;
    private Assembly myMouseAssembly;
    private Mass myMouseMass;
    private Spring myMouseSpring;

    /**
     * Initializes the canvas.
     *
     * @param size defines the size of the instantiated canvas
     */
    public Canvas (Dimension size) {
        // request component size
        setPreferredSize(size);
        // set component to receive user input
        setInputListeners();
        setFocusable(true);
        requestFocus();
        // create timer to drive the animation
        myTimer = new Timer(ONE_SECOND / FRAMES_PER_SECOND,
                new ActionListener() {
                    @Override
                    public void actionPerformed (ActionEvent e) {
                        step((double) FRAMES_PER_SECOND / ONE_SECOND);
                    }
                });
        loadEnvironment();
        myEnvironmentManager = new EnvironmentManager(this, myEnvironment);
        // initialize simulation
        myAssemblies = new ArrayList<Assembly>();
        loadModel();
    }

    /**
     * Starts the applet's action, i.e., starts the animation.
     */
    public void start () {
        myTimer.start();
    }

    /**
     * Take one step in the animation.
     *
     * @param elapsedTime duration of time to evaluate
     */
    public void step (double elapsedTime) {
        if (getLastMousePosition() != null) {
            dragMouseMass();
            myMouseSpring.update(null, elapsedTime);
        }
        myEnvironmentManager.updateMyWallLocations();
        for (Assembly assembly : myAssemblies) {
            assembly.update(elapsedTime);
        }
        // indirectly causes paint to be called
        repaint();
    }

    /**
     * Stops the applet's action, i.e., the animation.
     */
    public void stop () {
        myTimer.stop();
    }

    /**
     * Returns the last key pressed by the player (or -1 if none pressed).
     *
     * @see java.awt.event.KeyEvent
     */
    public int getLastKeyPressed () {
        return myLastKeyPressed;
    }

    /**
     * Returns the last position of the mouse in the canvas.
     */
    public Point getLastMousePosition () {
        return myLastMousePosition;
    }

    private void createUserSpring (Point point) {
        double minDist = Double.POSITIVE_INFINITY;
        int massID = 0;
        int assemblyIdx = 0;

        for (int i = 0; i < myAssemblies.size(); i++) {
            List<Mass> masses = myAssemblies.get(i).getMyMasses();
            for (Mass mass : masses) {
                double dist = Vector.distanceBetween(point, mass.getCenter());
                if (dist < minDist) {
                    minDist = dist;
                    massID = mass.getMyID();
                    assemblyIdx = i;
                }
            }
        }
        myMouseAssembly = myAssemblies.get(assemblyIdx);
        myMouseMass = new Mass(null, Integer.MAX_VALUE,
                myLastMousePosition.getX(), myLastMousePosition.getY(),
                Double.MAX_VALUE);
        Mass targetMass = myMouseAssembly.getMass(massID);
        myMouseSpring = new Spring(targetMass, myMouseMass,
                Vector.distanceBetween(targetMass.getCenter(),
                        myLastMousePosition), MOUSE_SPRING_CONSTANT);
        myMouseAssembly.add(myMouseSpring);
    }

    private void destroyUserSpring () {
        myMouseAssembly.getMyMasses().remove(myMouseMass);
        myMouseAssembly.getMySprings().remove(myMouseSpring);
        myMouseAssembly = null;
        myMouseMass = null;
        myMouseSpring = null;
    }

    private void dragMouseMass () {
        myMouseMass.setCenter(myLastMousePosition.getX(),
                myLastMousePosition.getY());
    }

    /**
     * Paint the contents of the canvas.
     *
     * Never called by you directly, instead called by Java runtime
     * when area of screen covered by this container needs to be
     * displayed (i.e., creation, uncovering, change in status)
     *
     * @param pen used to paint shape on the screen
     */
    @Override
    public void paintComponent (Graphics pen) {
        pen.setColor(Color.WHITE);
        pen.fillRect(0, 0, getSize().width, getSize().height);
        for (Assembly assembly : myAssemblies) {
            assembly.paint((Graphics2D) pen);
        }
    }

    /**
     * Create listeners that will update state based on user input.
     */
    private void setInputListeners () {
        myLastKeyPressed = NO_KEY_PRESSED;
        addKeyListener(new KeyAdapter() {
            @Override
            public void keyPressed (KeyEvent e) {
                myLastKeyPressed = e.getKeyCode();
                manageSimulationKeypress(myLastKeyPressed);
            }

            @Override
            public void keyReleased (KeyEvent e) {
                myLastKeyPressed = NO_KEY_PRESSED;
            }
        });

        myLastMousePosition = null;
        MouseInputAdapter mouseInputAdapter = new MouseInputAdapter() {
            @Override
            public void mousePressed (MouseEvent e) {
                myLastMousePosition = e.getPoint();
                createUserSpring(myLastMousePosition);
            }

            @Override
            public void mouseDragged (MouseEvent e) {
                myLastMousePosition = e.getPoint();
            }

            @Override
            public void mouseReleased (MouseEvent e) {
                myLastMousePosition = null;
                destroyUserSpring();
            }
        };

        addMouseListener(mouseInputAdapter);
        addMouseMotionListener(mouseInputAdapter);
    }

    private void loadModel () {
        final Factory factory = new Factory();
        int response = OUR_CHOOSER.showOpenDialog(null);
        if (response == JFileChooser.APPROVE_OPTION) {
            Assembly assembly = new Assembly(this, myEnvironmentManager);
            factory.loadModel(assembly, OUR_CHOOSER.getSelectedFile());
            myAssemblies.add(assembly);
        }
    }

    private void loadEnvironment () {
        myEnvironment = new EnvironmentFactory();
        int response = OUR_CHOOSER.showOpenDialog(null);
        if (response == JFileChooser.APPROVE_OPTION) {
            myEnvironment.loadEnvironment(OUR_CHOOSER.getSelectedFile());
        }
    }

    private void manageSimulationKeypress (int keyCode) {
        switch (keyCode) {
            case KeyEvent.VK_SPACE:
                if (myTimer.isRunning()) {
                    stop();
                }
                else {
                    start();
                }
                break;
            case KeyEvent.VK_S:
                step((double) FRAMES_PER_SECOND / ONE_SECOND);
                break;
            case KeyEvent.VK_P:
                for (Assembly assembly : myAssemblies) {
                    System.out.println(assembly);
                }
                break;
            case KeyEvent.VK_C:
                myAssemblies.clear();
                break;
            case KeyEvent.VK_N:
                loadModel();
                break;
            default:
                myEnvironmentManager.updateEnvironment(keyCode);
                break;
        }
    }
}
